﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace XNAProject
{
    class Camera : GameComponent
    {
        public enum CameraProjectionMode { Perspective, Orthographic }

        #region Members

        private Vector3 cameraPosition;
        private Vector3 cameraTarget;

        private float   fov;
        private float   rotationSpeed;
        private bool    readInput;

        private CameraProjectionMode projectionMode;

        private Matrix  view;
        private Matrix  projection;
        private Matrix  rotation;

        private bool    isCameraTop;
        private bool    orbitToTop;
        private float   originalXangle;

        private float   rotationX; // roll
        private float   rotationY; // yaw
        private float   rotationZ; // pitch

        private float   currentFixed;
        private bool    switchToFixed;
        private int     rotationQuadrant;

        private KeyboardState   currentKeyState;
        private MouseState      currentMouseState;

        private KeyboardState   previousKeyState;
        private MouseState      previousMouseState;

        #endregion

        #region Properties

        public float RotationX { get { return rotationX; } }
        public float RotationY { get { return rotationY; } }
        public float RotationZ { get { return rotationZ; } }

        public Matrix View       { get { return view; } }
        public Matrix Rotation   { get { return rotation; } }
        public Matrix Projection { get { return projection; } }

        public float FixedDegrees { get { return rotationSpeed; } }
        public float CurrentFixed { get { return currentFixed; } }
        public bool SwitchToFixed { get { return switchToFixed; } }
        public int RotationQuadrant { get { return rotationQuadrant; } }

        public Vector3 CameraPosition { get { return cameraPosition; } }

        private XnaGame _game;

        public bool ReadInput
        {
            get { return readInput; }
            set { readInput = value; }
        }

        public CameraProjectionMode ProjectionMode
        {
            get { return projectionMode; }
            set
            {
                projectionMode = value;
                Zoom(0f);
            }
        }

        #endregion

        public Camera(Vector3 position, Vector3 target, XnaGame game) : base(game)
        {
            _game = game;

            rotationX = 0f;
            rotationY = 0f;
            rotationZ = 0f;

            fov                 = 55;
            rotationSpeed       = 3f;
            rotationQuadrant    =  0;

            cameraPosition      = position;
            cameraTarget        = target;

            currentFixed        = 0f;
            switchToFixed       = false;
            orbitToTop          = false;

            readInput           = true;

            projectionMode      = CameraProjectionMode.Perspective;

            rotationX   = (float)Math.Atan(Math.Sqrt(cameraPosition.Z * cameraPosition.Z + cameraPosition.X * cameraPosition.X) / cameraPosition.Y);
            rotationX   = MathHelper.ToDegrees(rotationX);

            view        = Matrix.CreateLookAt(cameraPosition, cameraTarget, Vector3.Up);
            projection  = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(fov), game.GraphicsDevice.Viewport.AspectRatio, 0.1f, 25.0f);
            rotation    = Matrix.Identity;

        }

        public override void Update(GameTime gameTime)
        {
            if (readInput) HandleInput();

            if (isCameraTop && orbitToTop) OrbitUp();
            else if (!isCameraTop && !orbitToTop && rotationX + 0.1f < originalXangle)
            {
                // go back to original position until we meet the above condition
                OrbitDown();
            }
            else
            {
                orbitToTop = false;
                originalXangle = 0f;
            }

            if (rotationY == 0 || rotationY == 180 || rotationY == 360) rotationQuadrant = 0;
            else rotationQuadrant = (int) rotationY/90 + 1;

            if (switchToFixed)
            {
                if (rotationY != currentFixed)
                {
                    if (rotationY < currentFixed)
                        RotateYRelative(rotationSpeed);
                    else if (rotationY > currentFixed)
                        RotateYRelative(-rotationSpeed);

                    /*
                     * This will NEVER happen because
                     * rotationY < currentFixed is not true and
                     * rotationY > currentFixed is not true either
                     * 
                     * That means rotationY == currentFixed but the upper IF condition prevents this anyway
                     * 
                     */
                    //else
                    //    RotateYAbsolute(currentFixed);
                }
                else
                {
                    switchToFixed = false;
                }
            }

            // this has to be done .... ALWAYS!! Gna gna gna! Seriously, this must stay here.
            view = Matrix.CreateLookAt(cameraPosition, cameraTarget, Vector3.Up);

            base.Update(gameTime);
        }

        private void HandleInput()
        {
            currentKeyState = Keyboard.GetState();
            currentMouseState = Mouse.GetState();

            #region Zooming

            if (currentMouseState.ScrollWheelValue < previousMouseState.ScrollWheelValue)
            {
                Zoom(4);
            }

            if (currentKeyState[Keys.PageDown] == KeyState.Down)
            {
                Zoom(0.5f);
            }

            if (currentMouseState.ScrollWheelValue > previousMouseState.ScrollWheelValue)
            {
                Zoom(-4);
            }

            if (currentKeyState[Keys.PageUp] == KeyState.Down)
            {
                Zoom(-0.5f);
            }

            #endregion

            #region Movement

            if (currentKeyState[Keys.Right] == KeyState.Down)
            {
                RotateYRelative(rotationSpeed);
            }

            if (currentKeyState[Keys.Left] == KeyState.Down)
            {
                RotateYRelative(-rotationSpeed);
            }

            if (currentKeyState[Keys.Up] == KeyState.Down)
            {
                if (!isCameraTop) OrbitUp();
            }

            if (currentKeyState[Keys.Down] == KeyState.Down)
            {
                if (!isCameraTop) OrbitDown();
            }

            #endregion

            if (currentKeyState[Keys.R] == KeyState.Down && previousKeyState.IsKeyUp(Keys.R) ||
                currentMouseState.MiddleButton == ButtonState.Pressed && previousMouseState.MiddleButton == ButtonState.Released)
            {
                GotoQuadrant();
            }

            if (currentKeyState[Keys.T] == KeyState.Down && previousKeyState.IsKeyUp(Keys.T))
            {
                if (isCameraTop)
                {
                    isCameraTop = false;
                    orbitToTop = false;
                }
                else
                {
                    isCameraTop = true;
                    orbitToTop = true;
                    originalXangle = rotationX;

                    if (rotationQuadrant != 0 && !switchToFixed)
                        GotoQuadrant();
                }
            }

            if (currentKeyState[Keys.P] == KeyState.Down && previousKeyState.IsKeyUp(Keys.P))
            {
                if (projectionMode == CameraProjectionMode.Perspective)
                    projectionMode = CameraProjectionMode.Orthographic;
                else
                    projectionMode = CameraProjectionMode.Perspective;

                Zoom(0f);
            }

            previousKeyState = currentKeyState;
            previousMouseState = currentMouseState;
        }

        #region Rotate camera to a fixed angle

        public void RotateXAbsolute(float degrees)
        {
            rotation *= Matrix.CreateRotationX(MathHelper.ToRadians(degrees - rotationX));
            rotationX = degrees;
        }

        public void RotateYAbsolute(float degrees)
        {
            rotation *= Matrix.CreateRotationY(MathHelper.ToRadians(degrees - rotationY));
            rotationY = degrees;
        }

        public void RotateZAbsolute(float degrees)
        {
            rotation *= Matrix.CreateRotationZ(MathHelper.ToRadians(degrees - rotationZ));
            rotationZ = degrees;
        }

        #endregion

        #region Rotate camera with a relative angle

        public void RotateXRelative(float degrees)
        {
            rotationX += degrees;

            if (rotationX < 0) rotationX += 360;
            else if (rotationX > 360) rotationX -= 360;

            rotation *= Matrix.CreateRotationX(MathHelper.ToRadians(degrees));
        }

        public void RotateYRelative(float degrees)
        {
            rotationY += degrees;

            if (rotationY < 0) rotationY += 360;
            else if (rotationY > 360) rotationY -= 360;

            rotation *= Matrix.CreateRotationY(MathHelper.ToRadians(degrees));
        }

        public void RotateZRelative(float degrees)
        {
            rotationZ += degrees;

            if (rotationZ < 0) rotationZ += 360;
            else if (rotationZ > 360) rotationZ -= 360;

            rotation *= Matrix.CreateRotationZ(MathHelper.ToRadians(degrees));
        }

        #endregion

        #region Camera movement

        public void Up(float distance)
        {
            cameraPosition.Y += distance;
        }

        public void Down(float distance)
        {
            cameraPosition.Y -= distance;
        }

        public void Left(float distance)
        {
            cameraPosition.X -= distance;
        }

        public void Right(float distance)
        {
            cameraPosition.X += distance;
        }

        // NOT USED FOR THE TIME BEING
        /*
        public void Forward(float distance)
        {
            cameraPosition.Z -= distance;
            view *= Matrix.CreateTranslation(0f, 0f, -distance);

            //view *= Matrix.CreateScale(-distance);
        }

        public void Backward(float distance)
        {
            cameraPosition.Z += distance;
            view *= Matrix.CreateTranslation(0f, 0f, distance);

            //view *= Matrix.CreateScale(distance);
        }
        */

        #endregion

        public void Zoom(float factor)
        {
            // Restrict field of view between 20 and 120
            if (fov+factor < 20 || fov+factor > 120) return;

            fov += factor;

            switch (projectionMode)
            {
                case CameraProjectionMode.Perspective: projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(fov), 1.3f, 0.1f, 25.0f); break;
                case CameraProjectionMode.Orthographic: projection = Matrix.CreateOrthographic(fov * 0.20f, fov * 0.15f, 0.1f, 25f); break;
            }
        }

        public void OrbitUp()
        {
            float angle = (rotationX > rotationSpeed ? -rotationSpeed : -(rotationSpeed - rotationX));

            if (rotationX <= rotationSpeed)
            {
                rotationZ = 0;
                return;
            }

            Quaternion rotate = Quaternion.CreateFromAxisAngle(Vector3.UnitX, MathHelper.ToRadians(angle));
            cameraPosition = Vector3.Transform(cameraPosition, Matrix.CreateFromQuaternion(rotate));

            rotationX = (float)Math.Atan(Math.Sqrt(cameraPosition.Z * cameraPosition.Z + cameraPosition.X * cameraPosition.X) / cameraPosition.Y);
            rotationX = MathHelper.ToDegrees(rotationX);
        }

        public void OrbitDown()
        {
            float angle = Math.Min(89f-rotationX,rotationSpeed);

            if (rotationX < 89f)
            {
                Quaternion rotate = Quaternion.CreateFromAxisAngle(Vector3.UnitX, MathHelper.ToRadians(angle));
                cameraPosition = Vector3.Transform(cameraPosition, Matrix.CreateFromQuaternion(rotate));

                rotationX = (float)Math.Atan(Math.Sqrt(cameraPosition.Z * cameraPosition.Z + cameraPosition.X * cameraPosition.X) / cameraPosition.Y);
                rotationX = MathHelper.ToDegrees(rotationX);
            }
        }

        private void GotoQuadrant()
        {
            switch (rotationQuadrant)
            {
                case 0: currentFixed += 180; break;
                case 1: currentFixed = 0; break;
                case 2: currentFixed = 180; break;
                case 3: currentFixed = 180; break;
                case 4: currentFixed = 360; break;
            }

            if (currentFixed > 360)
            {
                currentFixed -= 360;
                rotationY -= 360;
            }

            switchToFixed = true;
        }
    }
}
